package nz.ac.massey.groupproject.ecs2;

import java.util.List;
import java.util.ArrayList;
import java.util.HashMap;

import java.util.WeakHashMap;

import java.lang.reflect.Method;
import java.lang.reflect.InvocationTargetException;


public class ECSSystemUtil {
	// TODO pass globalData to Systems nicely instead of passing it everywhere (access via ECSWorld? BYO pointer?)

	// ======================================== highlevel concept ========================================
	// world.commitChanges() is automatically called at the end of each System.
	// However, it is not called between multiple queries performed by the same System.

	public static void smartTick(List<ECSSystem> pipeline, ECSWorld world) {
		for (var sys : pipeline)
			smartTick(sys, world);
	}
	public static void smartTick(ECSSystem sys, ECSWorld world) {
		tryInvoke(sys, "globalTick", world);
		invokeOnMatchingEntities(sys, "tick", world);
		world.commitChanges();
	}







	// ======================================== smart invocation ========================================
	public static void tryInvoke(ECSSystem sys, String methName, ECSWorld world) {
		for (var meth: sys.getClass().getDeclaredMethods()) {

			if (! meth.getName().equals(methName))
				continue;
			tryInvoke(sys, meth, world);
		}
	}

	public static void tryInvoke(ECSSystem sys, Method meth, ECSWorld world) {
		Object globalData = world.globalData;
		int argc = meth.getParameterCount();
		var paramTs = meth.getGenericParameterTypes();

		if (0 == argc)
			callMethodAssertNoCheckedExceptions(sys, meth);
		else if (1 == argc && ECSWorld.class == paramTs[0])
			callMethodAssertNoCheckedExceptions(sys, meth, world);
		else if (1 == argc && globalData.getClass() == paramTs[0])
			callMethodAssertNoCheckedExceptions(sys, meth, globalData);
		// TODO else sys.meth(world, globalData)
		//      else sys.meth(globalData, world)
	}



	public static void invokeOnMatchingEntities(ECSSystem sys, String methName, ECSWorld world) {
		for (var meth : sys.getClass().getDeclaredMethods())
			if (meth.getName().equals(methName))
				invokeOnMatchingEntities(sys, meth, world);
	}

	public static void invokeOnMatchingEntities(ECSSystem sys, Method meth, ECSWorld world) {
		ECSQuery entityMatcher = getOrCreateCachedQuery(world, meth);
		if (null == entityMatcher) return;   // when assertions are enabled, an AssertionError will be thrown instead

		Object globalData = world.globalData;

		// prepare argument array
		var paramTs = (Class[]) meth.getGenericParameterTypes();
		int argc   = meth.getParameterCount();
		var args   = new Object[argc];
		var isComp = new boolean[argc];

		for (int ai = 0; ai < argc; ai++) {
			var paramT = paramTs[ai];
			if      (ECSWorld  .class      == paramT)           { isComp[ai] = false;  args[ai] = world;      }
			else if (globalData.getClass() == paramT)           { isComp[ai] = false;  args[ai] = globalData; }
			else if (ECSEntity .class      == paramT)             isComp[ai] = false;
			else {
				assert ECSComponent.class.isAssignableFrom(paramT)
					: "Internal Error: invalid System method signature should have been rejected by getOrCreateCachedQuery()";
				isComp[ai] = true;
			}
		}

		// loop and match
		for (var ent : entityMatcher.listMatches()) {
			for (int ai = 0; ai < argc; ai++) {
				var paramT = paramTs[ai];

				if (ECSEntity.class == paramT)
					args[ai] = ent;

				else if (isComp[ai]) {
					args[ai] = ent.getComponent(paramT);

					assert null != args[ai] : "Internal Error: A Query for a System returned non-matching Entity";
				}
				// else do nothing
			}

			callMethodAssertNoCheckedExceptions(sys, meth, args);
		}
	}





	// ======================================== cache ========================================
	// map (world, qtempl) -> query
	// map (world, method) -> query
	// Mildly confusing.  We cache Queries, which in turn cache matching Entities.
	//
	// when a world X is no longer used, remove all (X, qtempl) pairs and all (X, meth) pairs
	// XXX when a Query or a QueryTemplate (together with its Queries) is no longer used,
	//     it will not be GC'ed until the whole world is gone. (should we FIXME this?)
	//
	// TODO should we only use cached Query or allow user to request UncachedQuery?
	private static final WeakHashMap<ECSWorld, HashMap<ECSQueryTemplate, ECSQuery>>     cachedQueries      = new WeakHashMap<>();
	private static final WeakHashMap<ECSWorld, HashMap<Method,           ECSQuery>>     cachedQueryForMeth = new WeakHashMap<>();

	private static final ECSQuery getOrCreateCachedQuery(ECSWorld world, ECSQueryTemplate queryTempl) {
		var worldMap = cachedQueries.get(world);
		if (null == worldMap) {
			worldMap = new HashMap<>();
			cachedQueries.put(world, worldMap);
		}

		var query = worldMap.get(queryTempl);
		if (null == query) {
			query = new ECSQuery(world, queryTempl);
			worldMap.put(queryTempl, query);
		}
		return query;
	}

	/*
	 * If method signature is invalid ...
	 *    - and assertions are enabled, throws AssertionError
	 *    - but assertions are disabled, returns null
	 */
	private static final ECSQuery getOrCreateCachedQuery(ECSWorld world, Method meth) {
		var worldMap = cachedQueryForMeth.get(world);
		if (null != worldMap) {
			var query = worldMap.get(meth);
			if (null != query)
				return query;

		} else {
			worldMap = new HashMap<>();
			cachedQueryForMeth.put(world, worldMap);
		}

		assert worldMap != null && worldMap.containsKey(meth) == false;

		ArrayList<Class> compTypes = getRequiredComponentTypes(meth, world);
		if (null == compTypes) return null;
		var query = getOrCreateCachedQuery(world, ECSQueryTemplate.get(compTypes));
		worldMap.put(meth, query);
		return query;
	}

	// World is needed because, due to globalData's dynamically typed nature, a method signature
	// could be valid for one World but invalid for another World.
	private static final ArrayList<Class> getRequiredComponentTypes(Method meth, ECSWorld world) {
		var compTypes = new ArrayList<Class>();

		Object globalData = world.globalData;
		for (var paramT : (Class[]) meth.getGenericParameterTypes()) {
			if      (ECSWorld  .class      == paramT)  {}
			else if (globalData.getClass() == paramT)  {}
			else if (ECSEntity .class      == paramT)  {}

			else if (ECSComponent.class.isAssignableFrom(paramT))
				compTypes.add(paramT);

			else {
				assert false : "ECS: invalid System method signature";
				return null;
			}
		}
		return compTypes;
	}






	// ======================================== reflection utils ========================================
	// TODO move away

	// method should not throw checked exceptions
	public static Object callMethodAssertNoCheckedExceptions(Object receiver, Method meth, Object... args) {
		try {
			return meth.invoke(receiver, args);

		} catch (IllegalAccessException e) {
			throw new RuntimeException("Attempting to invoke inaccessible method (private, etc)", e);

		} catch (InvocationTargetException ee) {
			Throwable e = ee.getCause();
			if (e instanceof Error)             throw (Error) e;
			if (e instanceof RuntimeException)  throw (RuntimeException) e;
			throw new AssertionError("Methods that throw checked exceptions should not be invoked through reflection", e);
		}
	}
}
